// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-08-19

package gen_annotation

import (
	"fmt"
	"gitee.com/go-libs/console"
	"html/template"
	"os"
	"regexp"
	"strings"
)

var (
	SeekerRegexAllowedName  = regexp.MustCompile(`^[a-zA-Z]`)
	SeekerRegexFileIsGolang = regexp.MustCompile(`\.go$`)
)

// Seeker
// is a component for directory seeker.
type (
	// Seeker
	// is a component for seeker manager.
	Seeker struct {
		Command   *console.Command
		Container *console.Container
		Provider  *Provider

		SeekerController *SeekerController
		SeekerCrontab    *SeekerCrontab
		SeekerHandler    *SeekerHandler
		SeekerMiddleware *SeekerMiddleware
	}

	// SeekerDo
	// is a hook for directory scanner.
	SeekerDo func(path string) (err error)

	// SeekerFound
	// is an interface that called when source file found in a directory.
	SeekerFound interface {
		// Add
		// adds a file to seeker manager.
		Add(resource *Resource) (err error)

		// Do
		// generate and save result to target file.
		Do() (err error)

		// Target
		// returns a file path for generated content.
		Target() string
	}
)

func (o *Seeker) Do() (err error) {
	for _, x := range []SeekerFound{
		o.SeekerMiddleware,
		o.SeekerHandler,
		o.SeekerCrontab,
		o.SeekerController,
	} {
		if err = x.Do(); err != nil {
			break
		}
	}
	return
}

func (o *Seeker) DoController(path string) error {
	o.SeekerController = (&SeekerController{Seeker: o}).init()
	return o.scan(o.SeekerController, path)
}

func (o *Seeker) DoCrontab(path string) error {
	o.SeekerCrontab = (&SeekerCrontab{Seeker: o}).init()
	return o.scan(o.SeekerCrontab, path)
}

func (o *Seeker) DoHandler(path string) error {
	o.SeekerHandler = (&SeekerHandler{Seeker: o}).init()
	return o.scan(o.SeekerHandler, path)
}

func (o *Seeker) DoMiddleware(path string) error {
	o.SeekerMiddleware = (&SeekerMiddleware{Seeker: o}).init()
	return o.scan(o.SeekerMiddleware, path)
}

func (o *Seeker) Save(data any, target, text string) (err error) {
	var (
		file *os.File
		path = regexp.MustCompile(`/[^/]+$`).ReplaceAllString(target, "")
		tpl  *template.Template
	)

	// Make directory if necessary.
	if _, err = os.Stat(target); err != nil {
		if os.IsNotExist(err) {
			if err = os.MkdirAll(path, os.ModePerm); err != nil {
				return
			}
		} else {
			return
		}
	}

	// Open target file.
	if file, err = os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.ModePerm); err != nil {
		return
	}

	// Close file when done.
	defer func() {
		_ = file.Close()
	}()

	// Write content.
	if tpl, err = template.New("middleware").Parse(text); err == nil {
		err = tpl.Execute(file, data)
	}
	return
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *Seeker) scan(found SeekerFound, dir string) error {
	var (
		err  error
		info os.FileInfo
		path = dir
	)

	// Override able.
	if !o.Provider.Override {
		if _, err = os.Stat(found.Target()); err != nil {
			// Return error
			// for stat failed.
			if !os.IsNotExist(err) {
				return err
			}
		} else {
			// Return nil
			// for target file is existed.
			return nil
		}
	}

	// Validate path.
	if info, err = os.Stat(fmt.Sprintf(`%s/%s`, o.Provider.WorkingDir, path)); err != nil {
		// Return nil
		// if path not exists.
		if os.IsNotExist(err) {
			return nil
		}

		// Return error
		// if stat failed.
		return err
	}

	// Return error if specified path is a file.
	if !info.IsDir() {
		return fmt.Errorf(`invalid directory: %s`, dir)
	}

	// Read files in the path.
	if err = o.scanDirectory(found, path); err != nil {
		return err
	}
	return nil
}

func (o *Seeker) scanDirectory(found SeekerFound, path string, subs ...string) (err error) {
	var (
		dir  = fmt.Sprintf(`%s/%s`, o.Provider.WorkingDir, path)
		list []os.DirEntry
	)

	if subs == nil {
		subs = make([]string, 0)
	}

	// Build real directory with sub folders.
	for _, s := range subs {
		if s = strings.TrimSpace(s); s != "" {
			dir += fmt.Sprintf(`/%s`, s)
		}
	}

	// Read files in the path. Return error if failed.
	if list, err = os.ReadDir(dir); err != nil {
		return
	}

	// Iterate files and child folders.
	for _, entry := range list {
		// Ignore entries if name not match regex.
		if !SeekerRegexAllowedName.MatchString(entry.Name()) {
			continue
		}

		// Read child folder.
		if entry.IsDir() {
			if err = o.scanDirectory(found, path, append(subs, entry.Name())...); err != nil {
				return
			}
			continue
		}

		// Find go file.
		if SeekerRegexFileIsGolang.MatchString(entry.Name()) {
			// Create resource.
			res := NewResource(path, entry.Name())
			res.FillSrc(subs...)

			// Add to finder.
			if err = found.Add(res); err != nil {
				break
			}
		}
	}
	return
}
